Post by warpzone on Apr 11, 2017 8:52:47 GMT
I'm making a simple fast-paced action game with big, swooping attacks with huge hitboxes. (Or hitwedges might be a better term, since they're so big it would look unnatural if they weren't round. We're talking a semicircular radius that's as big around as the player's character is tall.)
The problem I'm facing right now is that when I attack a cluster of enemies, either ALL of them roll out of the way, or else NONE of them do. It looks unrealistic and artificial. How do I fix this?
When I set the roll% to, say, 0.05 (I.E. 1 in 20,) if I do a slash that hits 5 enemies, I would expect one of them to roll out of the way and the rest of them to get hit. Instead what happens is, if one of them rolls, all of them roll.
I have no idea what this is caused by, and casual googling didn't help. Any ideas where to start looking?
Update 1: Where does isRolling live?
We set the % chance to roll in the editor on the V_AI Controller script, so that's the logical place to start looking. Unfortunately, the script appears to begin with the "Iterations" and "On Change State Events" sections, so values like "Chance to Roll" that appear in the editor before those must be declared in another script. V_AIController.cs implements v_AIAnimator and vIMeleeFighter, so let's look in there.
v_AIAnimator includes the RollAnimation() function, which appears to be triggered alongside a bunch of other functions in UpdateAnimator(), which itself is public. But RollAnimation() doesn't seem to do a whole lot. There's some checking to ensure the animator exists and includes a base layer named Roll, then it disables what looks like anything that would allow player movement or disallow falling off a ledge. Everything else is similarly self-contained, and I don't see a declaration for isRolling even though it's used several places, so it must come from another script.
AIAnimator implements v_AIMotor, so let's have a look inside v_AIMotor.cs.
Jackpot. Here's where isRolling lives, as well as most of the rest of the Editor options. Line 93 contains those three beautiful words: public float chanceToRoll. The TakeDamage() method actually calls a method named CheckChanceToRoll() in the middle of some boolean logic to early-out of it. That looks promising. Let's check it out.
Update 2: How many PRNGs does it take to change a lightbulb?
Now, this is where it gets interesting. CheckChanceToRoll(), which, remember, gets called every single time a character is on the recieving end of an attack, creates a new RNG object-- not a new value, a new system to make values-- every single time. Wouldn't that slow down the AI a lot? Well, it turns out it's using Microsoft.net's System.Random, not Unity's UnityEngine.Random. A little googling found me this UnityAnswers page explaining the difference: forum.unity3d.com/threads/solved-system-random-vs-unityengine-random.327872/ Apparently you need to instantiate it just to use it. What concerns me, though, is the fact that this code doesn't appear to initialize the seed, nor does it seem to have any way of saving or comparing the previous values. So why doesn't every enemy perform the same actions in the same order every time they spawn?
Well, MSDN has this to say: msdn.microsoft.com/en-us/library/system.random(v=vs.110).aspx It seems that System.Random is, at its core, not seed-based like UnityEngine.Random, but rather a time-dependent PRNG. This means that every single fucking time this code gets called, it uses the system time to decide what to do. If it's time to Roll at 12:45 and 11 milliseconds, everybody who rolls the dice on whether or not to roll at 12:45 and 11 milliseconds will roll. One call to random.NextDouble() does not appear to affect what the next call to random.NextDouble() will return, at least if you insist on declaring it locally right before you use it. It's time-based, not seed-based.
Well... that's almost true. There's apparently a constructor with an optional parameter that will seed it, Random(Int32). Presumably with different seeds, different things will happen at 12:45 and 11 milliseconds. But what do we stuff in there? We can't use the current system time, because all the scripts get called at the same time so we're just moving the problem one level higher. If each enemy had some kinda unique ID, I could use that, but that seems like a lot of work to implement myself. In theory, we could use some arbitrary value like current world coordinates. But would the output "look" sufficiently random? I'd be converting a Float into an Int. I can't just cast it or round it, because then all the enemies standing near world coordinates 1,2,3 would all do the same thing at the same time. Maybe I could multiply it by 1000 and then round or cast? But thanks to floating point percision, there'd still be the same problem at a certain level. I have no idea what the random number table looks like or how well-distributed it is. Using "real world" values like world coordinates feels like begging for trouble. Shit like this is why Mersene Twister exists.
No, there's only one sane way I can think of to seed System.Random from this position in the file using Unity:
and that's just fucking stupid!
I need to get something to eat. I used up all my blood sugar getting this far. Stay tuned for the exciting conclusion in which I pick which kludge to use when bodging together Somebody Else's Code.
Update 3: So what are our options for the fix?
All right so we have three basic options to use to fix this problem. Seed System.Random with UnityEngine.Random... I hope I don't need to explain why that would be pants-on-head insane. Move the declaration of randomRoll to the top of the script so it's globally scoped to this script... which would put the "Next" back into random.NextDouble(), allowing it to pull values off of a list instead of initializing a new list every time CheckChanceToRoll() gets called, using it once, and then throwing it away... at the cost of slightly more memory usage at runtime. (Gotta store randomRoll somewhere, after all.) It would also still have problems whenever multiple enemies spawn at the same time and happen to require rolls on the same schedule as each other, which I could kinda see happening. If they both appeared simultaneously as the result of the player triggering an ambush, for example. The third option would be to delete all this MSDN bullshit and just use UnityEngine.Random.
Gonna be honest, here. I can't think of a single reason why Invector didn't just use UnityEngine.Random in the first place. It's automatically seeded by time and each value is different from the last, even if you call it multiple times in the same Update or even the same script. It's part of Unity, so there's no extra memory overhead required to invoke it. It's even faster to type! And once the decision was made to use System.Random, why use it in this way? Why not move it up to the top of the script so it can grab a new value every time? It's almost as if this code was meticulously sculpted to make all the enemies dodge at the same time. That, or Invector learned one specific way to do random numbers a few years ago in a C# programming class and brought all his bad habits with him to Unity.
I'm thinking about adding a public boolean to this script so I can choose between random methods when designing my game. One specific fight where the enemies have a 50% dodge chance and ALWAYS dodge as a group might be an interesting change of pace. Maybe I can use the art assets to telegraph that they're supposed to be clones or ninja or telepaths or something. But in the general case, no. Everyone can't dodge at the same time. That's literally ridiculous, and it makes your game look fake and cheap.
Switching to UnityEngine.Random for the general case.
If anyone can explain why System.Random was originally used the way it was, I'd love to hear it.
The problem I'm facing right now is that when I attack a cluster of enemies, either ALL of them roll out of the way, or else NONE of them do. It looks unrealistic and artificial. How do I fix this?
When I set the roll% to, say, 0.05 (I.E. 1 in 20,) if I do a slash that hits 5 enemies, I would expect one of them to roll out of the way and the rest of them to get hit. Instead what happens is, if one of them rolls, all of them roll.
I have no idea what this is caused by, and casual googling didn't help. Any ideas where to start looking?
Update 1: Where does isRolling live?
We set the % chance to roll in the editor on the V_AI Controller script, so that's the logical place to start looking. Unfortunately, the script appears to begin with the "Iterations" and "On Change State Events" sections, so values like "Chance to Roll" that appear in the editor before those must be declared in another script. V_AIController.cs implements v_AIAnimator and vIMeleeFighter, so let's look in there.
v_AIAnimator includes the RollAnimation() function, which appears to be triggered alongside a bunch of other functions in UpdateAnimator(), which itself is public. But RollAnimation() doesn't seem to do a whole lot. There's some checking to ensure the animator exists and includes a base layer named Roll, then it disables what looks like anything that would allow player movement or disallow falling off a ledge. Everything else is similarly self-contained, and I don't see a declaration for isRolling even though it's used several places, so it must come from another script.
AIAnimator implements v_AIMotor, so let's have a look inside v_AIMotor.cs.
Jackpot. Here's where isRolling lives, as well as most of the rest of the Editor options. Line 93 contains those three beautiful words: public float chanceToRoll. The TakeDamage() method actually calls a method named CheckChanceToRoll() in the middle of some boolean logic to early-out of it. That looks promising. Let's check it out.
Update 2: How many PRNGs does it take to change a lightbulb?
Now, this is where it gets interesting. CheckChanceToRoll(), which, remember, gets called every single time a character is on the recieving end of an attack, creates a new RNG object-- not a new value, a new system to make values-- every single time. Wouldn't that slow down the AI a lot? Well, it turns out it's using Microsoft.net's System.Random, not Unity's UnityEngine.Random. A little googling found me this UnityAnswers page explaining the difference: forum.unity3d.com/threads/solved-system-random-vs-unityengine-random.327872/ Apparently you need to instantiate it just to use it. What concerns me, though, is the fact that this code doesn't appear to initialize the seed, nor does it seem to have any way of saving or comparing the previous values. So why doesn't every enemy perform the same actions in the same order every time they spawn?
Well, MSDN has this to say: msdn.microsoft.com/en-us/library/system.random(v=vs.110).aspx It seems that System.Random is, at its core, not seed-based like UnityEngine.Random, but rather a time-dependent PRNG. This means that every single fucking time this code gets called, it uses the system time to decide what to do. If it's time to Roll at 12:45 and 11 milliseconds, everybody who rolls the dice on whether or not to roll at 12:45 and 11 milliseconds will roll. One call to random.NextDouble() does not appear to affect what the next call to random.NextDouble() will return, at least if you insist on declaring it locally right before you use it. It's time-based, not seed-based.
Well... that's almost true. There's apparently a constructor with an optional parameter that will seed it, Random(Int32). Presumably with different seeds, different things will happen at 12:45 and 11 milliseconds. But what do we stuff in there? We can't use the current system time, because all the scripts get called at the same time so we're just moving the problem one level higher. If each enemy had some kinda unique ID, I could use that, but that seems like a lot of work to implement myself. In theory, we could use some arbitrary value like current world coordinates. But would the output "look" sufficiently random? I'd be converting a Float into an Int. I can't just cast it or round it, because then all the enemies standing near world coordinates 1,2,3 would all do the same thing at the same time. Maybe I could multiply it by 1000 and then round or cast? But thanks to floating point percision, there'd still be the same problem at a certain level. I have no idea what the random number table looks like or how well-distributed it is. Using "real world" values like world coordinates feels like begging for trouble. Shit like this is why Mersene Twister exists.
No, there's only one sane way I can think of to seed System.Random from this position in the file using Unity:
var randomRoll = (float)System.Random.NextDouble( UnityEngine.Random.Range(0 , int.MaxValue) );
and that's just fucking stupid!
I need to get something to eat. I used up all my blood sugar getting this far. Stay tuned for the exciting conclusion in which I pick which kludge to use when bodging together Somebody Else's Code.
Update 3: So what are our options for the fix?
All right so we have three basic options to use to fix this problem. Seed System.Random with UnityEngine.Random... I hope I don't need to explain why that would be pants-on-head insane. Move the declaration of randomRoll to the top of the script so it's globally scoped to this script... which would put the "Next" back into random.NextDouble(), allowing it to pull values off of a list instead of initializing a new list every time CheckChanceToRoll() gets called, using it once, and then throwing it away... at the cost of slightly more memory usage at runtime. (Gotta store randomRoll somewhere, after all.) It would also still have problems whenever multiple enemies spawn at the same time and happen to require rolls on the same schedule as each other, which I could kinda see happening. If they both appeared simultaneously as the result of the player triggering an ambush, for example. The third option would be to delete all this MSDN bullshit and just use UnityEngine.Random.
Gonna be honest, here. I can't think of a single reason why Invector didn't just use UnityEngine.Random in the first place. It's automatically seeded by time and each value is different from the last, even if you call it multiple times in the same Update or even the same script. It's part of Unity, so there's no extra memory overhead required to invoke it. It's even faster to type! And once the decision was made to use System.Random, why use it in this way? Why not move it up to the top of the script so it can grab a new value every time? It's almost as if this code was meticulously sculpted to make all the enemies dodge at the same time. That, or Invector learned one specific way to do random numbers a few years ago in a C# programming class and brought all his bad habits with him to Unity.
I'm thinking about adding a public boolean to this script so I can choose between random methods when designing my game. One specific fight where the enemies have a 50% dodge chance and ALWAYS dodge as a group might be an interesting change of pace. Maybe I can use the art assets to telegraph that they're supposed to be clones or ninja or telepaths or something. But in the general case, no. Everyone can't dodge at the same time. That's literally ridiculous, and it makes your game look fake and cheap.
Switching to UnityEngine.Random for the general case.
If anyone can explain why System.Random was originally used the way it was, I'd love to hear it.